#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <signal.h>
#include <fcntl.h>
#include <stdio.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/file.h>
#include <sys/ioctl.h>
#include <sys/wait.h>
#include <sys/resource.h>
#include <sys/stat.h>
#include <netdb.h>
#include <syslog.h>
#include "open.h"

#define FILTER          "/etc/proxy.filter"
#define MAXMASK         128
#define PROXYD          "proxyd"
#define TCP_PROTO       "tcp"
#define BUFSIZE         8192
#define OVERTIME        300
#define LOCKFILE        "/var/run/proxyd.pid"
#define LOCKMODE        (S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP)

/* mask host addr */
struct in_addr maskaddr[MAXMASK];
int maskcnt;

struct header {
    char method[BUFSIZE];
    char url[BUFSIZE];
    char version[BUFSIZE];
    struct sockaddr_in servaddr;
    int connection;
    int overtime;
};

void parse_header(char *header, struct header *hp);
void daemonize(int servfd);
void do_proxy(int userfd, struct sockaddr_in *cliaddr);
void sig_chld(int signo);
void sig_term(int signo);
void sig_hup(int signo);
void errorout(char *msg);
void reread(void);
int already_running(void);

void sig_chld(int signo)
{
    while (waitpid(-1, NULL, WNOHANG) > 0);
}

void sig_hup(int signo)
{
    syslog(LOG_INFO, "re-reading filter file");
    reread();
}

void sig_term(int signo)
{
    syslog(LOG_INFO, "got SIGTERM, exiting");
    exit(0);
}

void errorout(char *msg)
{
    syslog(LOG_ERR, "%s: %m", msg);
    exit(1);
}

int main(int argc, char *argv[])
{
    pid_t childpid;
    int clilen, listenfd, connfd;
    struct sockaddr_in cliaddr;
    struct sigaction sa;
    int proxy_port;

    if (argc != 2) {
        fprintf(stderr, "usage: %s <port>\n", argv[0]);
        exit(0);
    }
    proxy_port = atoi(argv[1]);
        
    /* create listenfd for client */
    if ((listenfd = open_listenfd(proxy_port)) < 0) {
        perror("create listenfd error");
        exit(0);
    }

    /* turn ourselves into a daemon */
    daemonize(listenfd);

    /* make sure only one copy of the daemon is running */
    if (already_running())
        errorout(PROXYD " alreading running");

    /* handle signals of interest */
    sa.sa_handler = sig_term;
    sigemptyset(&sa.sa_mask);
    sigaddset(&sa.sa_mask, SIGHUP);
    sigaddset(&sa.sa_mask, SIGCHLD);
    sa.sa_flags = 0;
    if (sigaction(SIGTERM, &sa, NULL) < 0)
        errorout("catch SIGTERM error");

    sa.sa_handler = sig_chld;
    sigemptyset(&sa.sa_mask);
    sigaddset(&sa.sa_mask, SIGHUP);
    sigaddset(&sa.sa_mask, SIGTERM);
    sa.sa_flags = 0;
    if (sigaction(SIGCHLD, &sa, NULL) < 0)
        errorout("catch SIGCHLD error");

    sa.sa_handler = sig_hup;
    sigemptyset(&sa.sa_mask);
    sigaddset(&sa.sa_mask, SIGCHLD);
    sigaddset(&sa.sa_mask, SIGTERM);
    sa.sa_flags = 0;
    if (sigaction(SIGHUP, &sa, NULL) < 0)
        errorout("catch SIGHUP error");

    /* read the mask host addr */
    reread();

    /* fall into a loop to accept new connection and spawn children */
    while (1) {
        /* accept the next connection */    
        clilen = sizeof(cliaddr);
        connfd = accept(listenfd, (struct sockaddr *)&cliaddr, &clilen);

        /* a signal might interrupt accept call */
        if (connfd < 0 && errno == EINTR)
            continue;
        else if (connfd < 0)
            errorout("accept connection error");

        /* fork a child to handle this connection */
        if ((childpid = fork()) == 0) {
            close(listenfd);
            do_proxy(connfd, &cliaddr);
            exit(0);
        }

        close(connfd);
    }

    return 0;
}

/* parse the http header */
void parse_header(char *header, struct header *hp)
{
    unsigned long inaddr;
    struct hostent *hostp;
    char buf[BUFSIZE], *str, *port; 

    hp->connection = 1;
    memset(&hp->servaddr, 0, sizeof(struct sockaddr));
    hp->servaddr.sin_family = AF_INET;
    
    strncpy(buf, header, strlen(header)+1);
    strtok(buf, "\r\n");
    sscanf(buf, "%s %s %s", hp->method, hp->url, hp->version);

    while (str = strtok(NULL, "\r\n")) {
        if (!strncasecmp(str, "host:", sizeof("host:")-1)) {
            str += sizeof("host:");
            if ((port = index(str, ':')) != NULL) {
                *port = '\0';
                port ++;
            }

            if (inet_aton(str, (struct in_addr *)&inaddr) != 0)
                hp->servaddr.sin_addr.s_addr = htonl(inaddr);
            else if ((hostp = gethostbyname(str)) != NULL)
                memcpy(&hp->servaddr.sin_addr.s_addr, hostp->h_addr, 
                    hostp->h_length);
            if (port == NULL)
                hp->servaddr.sin_port = htons(80);
            else
                hp->servaddr.sin_port = htons(atoi(port));
        }
        else if (!strncasecmp(str, "connection:", sizeof("connection:")-1)) {
            str += sizeof("connection:"); 
            if (!strncasecmp(str, "close", sizeof("close")-1))
                hp->connection = 0;
        }
        else if (!strncasecmp(str, "keep-alive:", sizeof("keep-alive:")-1)) {
            str += sizeof("keep-alive:");
            hp->overtime = atoi(str); 
        }
    }
}
        
/* daemon process */
void daemonize(int servfd)
{
    pid_t childpid;
    int i;
    struct rlimit rl;
    struct sigaction action;

    umask(0);

    if (getrlimit(RLIMIT_NOFILE, &rl) != 0) {
        perror("getrlimit error");
        exit(1);
    }

    if ((childpid = fork()) < 0) {
        perror("fork error");
        exit(1);
    }
    else if (childpid > 0)
        exit(0);
    else
        setsid();

    action.sa_handler = SIG_IGN;
    sigemptyset(&action.sa_mask);
    action.sa_flags = 0;
    if (sigaction(SIGHUP, &action, NULL) < 0) {
        perror("sigaction error");
        exit(1);
    }

    if ((childpid = fork()) < 0) {
        perror("fork error");
        exit(1);
    }
    else if (childpid > 0)
        exit(0);

    if (chdir("/") < 0) {
        perror("chdir error");
        exit(1);
    }

    if (rl.rlim_max == RLIM_INFINITY)
        rl.rlim_max = 1024;

    for (i = 0; i < rl.rlim_max; i++)
        if (i != servfd)
            close(i);

    openlog(PROXYD, LOG_CONS, LOG_DAEMON);
}

int already_running()
{
    int fd;
    char buf[16];
    struct flock fl;

    if ((fd = open(LOCKFILE, O_RDWR|O_CREAT, LOCKMODE)) < 0) 
        errorout("open " LOCKFILE " error");
        
    fl.l_type = F_WRLCK;
    fl.l_start = 0;
    fl.l_whence = SEEK_SET;
    fl.l_len = 0;
    if (fcntl(fd, F_SETLK, &fl) < 0) {
        if (errno == EACCES || errno == EAGAIN) {
            close(fd);
            return 1;
        }
        errorout("lock " LOCKFILE " error");
    }

    ftruncate(fd, 0);
    sprintf(buf, "%ld", (long)getpid());
    write(fd, buf, strlen(buf)+1);
    
    return 0;
}

void do_proxy(int userfd, struct sockaddr_in *cliaddr)
{
    int i, servfd, iolen, connstat, servport, ret, conn;
    struct sockaddr_in *servaddr;
    fd_set readset, readyset;
    struct timeval tv;
    char buf[BUFSIZE], tmp[BUFSIZE];
    struct header h;

    FD_ZERO(&readset);
    FD_SET(userfd, &readset);

    tv.tv_sec = OVERTIME;
    tv.tv_usec = 0;

    servfd = 0;
    conn = 1;
    while (1) {
        readyset = readset;
        if ((ret = select(FD_SETSIZE, &readyset, NULL, NULL, &tv)) <= 0) {
            if (ret == 0) {     /* over time */
                syslog(LOG_INFO, "connect timeout");
                break; 
            }
            else
                errorout("select error");
        }

        if (FD_ISSET(userfd, &readyset)) {
            if ((iolen = read(userfd, buf, BUFSIZE)) <= 0)
                break;
            
            /* parse the http header */
            parse_header(buf, &h);
            conn = h.connection;
            if (conn == 1)
                tv.tv_sec = h.overtime;

            syslog(LOG_INFO, "conntect to %s:%d", inet_ntoa(h.servaddr.sin_addr),
                ntohs(h.servaddr.sin_port));

            for (i = 0; i < maskcnt; i++) {
                if (maskaddr[i].s_addr == h.servaddr.sin_addr.s_addr) {
                    sprintf(buf, "<html><body bgcolor=ffffff>");         
                    sprintf(buf, "%s<p>%s mask</body></html>", buf, 
                        inet_ntoa(maskaddr[i]));         
            
                    sprintf(tmp, "HTTP/1.0 %s %s\r\n", "403", "Forbidden");
                    write(userfd, tmp, strlen(tmp));
                    sprintf(tmp, "Content-type: text/html\r\n");
                    write(userfd, tmp, strlen(tmp));
                    sprintf(tmp, "Content-length: %d\r\n\r\n", strlen(buf));
                    write(userfd, tmp, strlen(tmp));
                    write(userfd, buf, strlen(buf));

                    syslog(LOG_INFO, "%s mask", inet_ntoa(maskaddr[i]));
                    
                    close(userfd);
                    return ;
                }
            }

            /* connect to server if not connected */
            if (servfd == 0) {
                /* open a socket to connect the server */
                if ((servfd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
                    errorout("create socket to server error");

                /* attempt a connection */
                connstat = connect(servfd, (struct sockaddr *)&h.servaddr, 
                    sizeof(struct sockaddr));

                switch (connstat) {
                    case 0:
                        break;
                    case ETIMEDOUT:
                    case ECONNREFUSED:
                    case ENETUNREACH:
                        strncpy(buf, strerror(errno), BUFSIZE);
                        write(userfd, buf, strlen(buf));
                        close(userfd);
                        exit(1);
                    default:
                        errorout("connect to host error");
                }

                FD_SET(servfd, &readset);
            }

            /* copy to host */
            write(servfd, buf, iolen);

            /* get the url */
            syslog(LOG_INFO, "client %s request: %s", 
                inet_ntoa(cliaddr->sin_addr), h.url);
        }

        if (FD_ISSET(servfd, &readyset)) {
            if ((iolen = read(servfd, buf, BUFSIZE)) <= 0)
                break;

            /* copy to client */
            write(userfd, buf, iolen);

            if (conn != 1)
                break;
        }
    }

    if (servfd != 0)
        close(servfd);
    close(userfd);
}

void reread()
{
    FILE *fp;
    char buf[BUFSIZE];
    struct hostent *hostp;
    unsigned long inaddr;
    int i, len;

    if ((fp = fopen(FILTER, "r")) == NULL)
        errorout("fopen " FILTER " error");

    i = 0;
    while ((fgets(buf, BUFSIZE, fp) != NULL) && (i < MAXMASK)) {
        /* move the '\n' at the last of buf */
        len = strlen(buf);
        if (len == 1)
            break;
        else
            buf[len-1] = '\0';

        if (inet_aton(buf, (struct in_addr *)&inaddr) != 0)
            maskaddr[i].s_addr = htonl(inaddr);
        else if ((hostp = gethostbyname(buf)) != NULL)
            memcpy(&maskaddr[i].s_addr, hostp->h_addr, hostp->h_length);
        else
            syslog(LOG_INFO, "unknown mask host addr: %s:%s", buf, 
                hstrerror(h_errno));

        i++;
    }
    maskcnt = i;
}
